Completed
Push — main ( 920eff...bf1b59 )
by Alejandro
20s queued 11s
created

ShlinkApiClient.health   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
crap 1
1
import qs from 'qs';
2
import { isEmpty, isNil, reject } from 'ramda';
3
import { AxiosInstance, AxiosResponse, Method } from 'axios';
4
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams';
5
import { ShortUrl, ShortUrlData } from '../../short-urls/data';
6
import { OptionalString } from '../utils';
7
import {
8
  ShlinkHealth,
9
  ShlinkMercureInfo,
10
  ShlinkShortUrlsResponse,
11
  ShlinkTags,
12
  ShlinkTagsResponse,
13
  ShlinkVisits,
14
  ShlinkVisitsParams,
15
  ShlinkShortUrlMeta,
16
  ShlinkDomain,
17
  ShlinkDomainsResponse,
18
  ShlinkVisitsOverview,
19
} from './types';
20
21 24
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
22 2
const rejectNilProps = reject(isNil);
23
24
export default class ShlinkApiClient {
25
  private apiVersion: number;
26
27
  public constructor(
28
    private readonly axios: AxiosInstance,
29
    private readonly baseUrl: string,
30
    private readonly apiKey: string,
31
  ) {
32 28
    this.apiVersion = 2;
33
  }
34
35 28
  public readonly listShortUrls = async (params: ShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> =>
36 1
    this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', params)
37 1
      .then(({ data }) => data.shortUrls);
38
39 28
  public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => {
40 4
    const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any);
41
42 2
    return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions)
43 2
      .then((resp) => resp.data);
44
  };
45
46 28
  public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> =>
47 1
    this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query)
48 1
      .then(({ data }) => data.visits);
49
50 28
  public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
51 1
    this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
52 1
      .then(({ data }) => data.visits);
53
54 28
  public readonly getVisitsOverview = async (): Promise<ShlinkVisitsOverview> =>
55 1
    this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits', 'GET')
56 1
      .then(({ data }) => data.visits);
57
58 28
  public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> =>
59 3
    this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain })
60 3
      .then(({ data }) => data);
61
62 28
  public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> =>
63 3
    this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
64
      .then(() => {});
65
66 28
  public readonly updateShortUrlTags = async (
67
    shortCode: string,
68
    domain: OptionalString,
69
    tags: string[],
70
  ): Promise<string[]> =>
71 3
    this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
72 3
      .then(({ data }) => data.tags);
73
74 28
  public readonly updateShortUrlMeta = async (
75
    shortCode: string,
76
    domain: OptionalString,
77
    meta: ShlinkShortUrlMeta,
78
  ): Promise<ShlinkShortUrlMeta> =>
79 3
    this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
80 3
      .then(() => meta);
81
82 28
  public readonly listTags = async (): Promise<ShlinkTags> =>
83 1
    this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
84 1
      .then((resp) => resp.data.tags)
85 1
      .then(({ data, stats }) => ({ tags: data, stats }));
86
87 28
  public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
88 1
    this.performRequest('/tags', 'DELETE', { tags })
89 1
      .then(() => ({ tags }));
90
91 28
  public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> =>
92 1
    this.performRequest('/tags', 'PUT', {}, { oldName, newName })
93 1
      .then(() => ({ oldName, newName }));
94
95 28
  public readonly health = async (): Promise<ShlinkHealth> =>
96 1
    this.performRequest<ShlinkHealth>('/health', 'GET')
97 1
      .then((resp) => resp.data);
98
99 28
  public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> =>
100 1
    this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET')
101 1
      .then((resp) => resp.data);
102
103 28
  public readonly listDomains = async (): Promise<ShlinkDomain[]> =>
104 1
    this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data);
105
106 28
  private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => {
107 24
    try {
108 24
      return await this.axios({
109
        method,
110
        url: `${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}`,
111
        headers: { 'X-Api-Key': this.apiKey },
112
        params: rejectNilProps(query),
113
        data: body,
114
        paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
115
      });
116
    } catch (e) {
117
      const { response } = e;
118
119
      // Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error
120
      // when performed from the browser (due to the preflight request not returning a 2xx status.
121
      // See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here.
122
      // The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as
123
      // if a request has been performed to a not supported API version.
124
      const apiVersionIsNotSupported = !response;
125
126
      // When the request is not invalid or we have already tried both API versions, throw the error and let the
127
      // caller handle it
128 4
      if (!apiVersionIsNotSupported || this.apiVersion === 1) {
129
        throw e;
130
      }
131
132
      this.apiVersion = this.apiVersion - 1;
133
134
      return await this.performRequest(url, method, query, body);
135
    }
136
  };
137
}
138